package ipfshttp

import (
	"encoding/json"
	"errors"
	"fmt"
	"time"

	"github.com/kelseyhightower/envconfig"

	"github.com/ipfs/ipfs-cluster/config"

	ma "github.com/multiformats/go-multiaddr"
)

const configKey = "ipfshttp"
const envConfigKey = "cluster_ipfshttp"

// Default values for Config.
const (
	DefaultNodeAddr           = "/ip4/127.0.0.1/tcp/5001"
	DefaultConnectSwarmsDelay = 30 * time.Second
	DefaultIPFSRequestTimeout = 5 * time.Minute
	DefaultPinTimeout         = 2 * time.Minute
	DefaultUnpinTimeout       = 3 * time.Hour
	DefaultRepoGCTimeout      = 24 * time.Hour
	DefaultUnpinDisable       = false
)

// Config is used to initialize a Connector and allows to customize
// its behaviour. It implements the config.ComponentConfig interface.
type Config struct {
	config.Saver

	// Host/Port for the IPFS daemon.
	NodeAddr ma.Multiaddr

	// ConnectSwarmsDelay specifies how long to wait after startup before
	// attempting to open connections from this peer's IPFS daemon to the
	// IPFS daemons of other peers.
	ConnectSwarmsDelay time.Duration

	// IPFS Daemon HTTP Client POST timeout
	IPFSRequestTimeout time.Duration

	// Pin Operation timeout
	PinTimeout time.Duration

	// Unpin Operation timeout
	UnpinTimeout time.Duration

	// RepoGC Operation timeout
	RepoGCTimeout time.Duration
	// Disables the unpin operation and returns an error.
	UnpinDisable bool

	// Tracing flag used to skip tracing specific paths when not enabled.
	Tracing bool
}

type jsonConfig struct {
	NodeMultiaddress   string `json:"node_multiaddress"`
	ConnectSwarmsDelay string `json:"connect_swarms_delay"`
	IPFSRequestTimeout string `json:"ipfs_request_timeout"`
	PinTimeout         string `json:"pin_timeout"`
	UnpinTimeout       string `json:"unpin_timeout"`
	RepoGCTimeout      string `json:"repogc_timeout"`
	UnpinDisable       bool   `json:"unpin_disable,omitempty"`
}

// ConfigKey provides a human-friendly identifier for this type of Config.
func (cfg *Config) ConfigKey() string {
	return configKey
}

// Default sets the fields of this Config to sensible default values.
func (cfg *Config) Default() error {
	node, _ := ma.NewMultiaddr(DefaultNodeAddr)
	cfg.NodeAddr = node
	cfg.ConnectSwarmsDelay = DefaultConnectSwarmsDelay
	cfg.IPFSRequestTimeout = DefaultIPFSRequestTimeout
	cfg.PinTimeout = DefaultPinTimeout
	cfg.UnpinTimeout = DefaultUnpinTimeout
	cfg.RepoGCTimeout = DefaultRepoGCTimeout
	cfg.UnpinDisable = DefaultUnpinDisable

	return nil
}

// ApplyEnvVars fills in any Config fields found
// as environment variables.
func (cfg *Config) ApplyEnvVars() error {
	jcfg, err := cfg.toJSONConfig()
	if err != nil {
		return err
	}

	err = envconfig.Process(envConfigKey, jcfg)
	if err != nil {
		return err
	}

	return cfg.applyJSONConfig(jcfg)
}

// Validate checks that the fields of this Config have sensible values,
// at least in appearance.
func (cfg *Config) Validate() error {
	var err error
	if cfg.NodeAddr == nil {
		err = errors.New("ipfshttp.node_multiaddress not set")
	}

	if cfg.ConnectSwarmsDelay < 0 {
		err = errors.New("ipfshttp.connect_swarms_delay is invalid")
	}

	if cfg.IPFSRequestTimeout < 0 {
		err = errors.New("ipfshttp.ipfs_request_timeout invalid")
	}

	if cfg.PinTimeout < 0 {
		err = errors.New("ipfshttp.pin_timeout invalid")
	}

	if cfg.UnpinTimeout < 0 {
		err = errors.New("ipfshttp.unpin_timeout invalid")
	}

	if cfg.RepoGCTimeout < 0 {
		err = errors.New("ipfshttp.repogc_timeout invalid")
	}

	return err

}

// LoadJSON parses a JSON representation of this Config as generated by ToJSON.
func (cfg *Config) LoadJSON(raw []byte) error {
	jcfg := &jsonConfig{}
	err := json.Unmarshal(raw, jcfg)
	if err != nil {
		logger.Error("Error unmarshaling ipfshttp config")
		return err
	}

	cfg.Default()

	return cfg.applyJSONConfig(jcfg)
}

func (cfg *Config) applyJSONConfig(jcfg *jsonConfig) error {
	nodeAddr, err := ma.NewMultiaddr(jcfg.NodeMultiaddress)
	if err != nil {
		return fmt.Errorf("error parsing ipfs_node_multiaddress: %s", err)
	}

	cfg.NodeAddr = nodeAddr
	cfg.UnpinDisable = jcfg.UnpinDisable

	err = config.ParseDurations(
		"ipfshttp",
		&config.DurationOpt{Duration: jcfg.ConnectSwarmsDelay, Dst: &cfg.ConnectSwarmsDelay, Name: "connect_swarms_delay"},
		&config.DurationOpt{Duration: jcfg.IPFSRequestTimeout, Dst: &cfg.IPFSRequestTimeout, Name: "ipfs_request_timeout"},
		&config.DurationOpt{Duration: jcfg.PinTimeout, Dst: &cfg.PinTimeout, Name: "pin_timeout"},
		&config.DurationOpt{Duration: jcfg.UnpinTimeout, Dst: &cfg.UnpinTimeout, Name: "unpin_timeout"},
		&config.DurationOpt{Duration: jcfg.RepoGCTimeout, Dst: &cfg.RepoGCTimeout, Name: "repogc_timeout"},
	)
	if err != nil {
		return err
	}

	return cfg.Validate()
}

// ToJSON generates a human-friendly JSON representation of this Config.
func (cfg *Config) ToJSON() (raw []byte, err error) {
	jcfg, err := cfg.toJSONConfig()
	if err != nil {
		return
	}

	raw, err = config.DefaultJSONMarshal(jcfg)
	return
}

func (cfg *Config) toJSONConfig() (jcfg *jsonConfig, err error) {
	// Multiaddress String() may panic
	defer func() {
		if r := recover(); r != nil {
			err = fmt.Errorf("%s", r)
		}
	}()

	jcfg = &jsonConfig{}

	// Set all configuration fields
	jcfg.NodeMultiaddress = cfg.NodeAddr.String()
	jcfg.ConnectSwarmsDelay = cfg.ConnectSwarmsDelay.String()
	jcfg.IPFSRequestTimeout = cfg.IPFSRequestTimeout.String()
	jcfg.PinTimeout = cfg.PinTimeout.String()
	jcfg.UnpinTimeout = cfg.UnpinTimeout.String()
	jcfg.RepoGCTimeout = cfg.RepoGCTimeout.String()
	jcfg.UnpinDisable = cfg.UnpinDisable

	return
}

// ToDisplayJSON returns JSON config as a string.
func (cfg *Config) ToDisplayJSON() ([]byte, error) {
	jcfg, err := cfg.toJSONConfig()
	if err != nil {
		return nil, err
	}

	return config.DisplayJSON(jcfg)
}
